package com.gitee.wsl.struct.grammar.ast.render


import com.gitee.wsl.struct.grammar.ast.node.ASTNode
import com.gitee.wsl.struct.grammar.ast.node.LeafNode
import com.gitee.wsl.struct.grammar.ast.node.NodeType
import com.gitee.wsl.struct.grammar.ast.node.acceptChildren
import com.gitee.wsl.struct.grammar.ast.node.getTextInNode
import com.gitee.wsl.struct.grammar.ast.render.provider.GeneratingProvider
import com.gitee.wsl.struct.grammar.ast.render.tag.TagRenderer
import com.gitee.wsl.struct.grammar.ast.render.tag.TagVisitor

typealias AttributesCustomizer = (node: ASTNode, tagName: CharSequence, attributes: Iterable<CharSequence?>) -> Iterable<CharSequence?>

val DUMMY_ATTRIBUTES_CUSTOMIZER: AttributesCustomizer = { _, _, attributes -> attributes }

open class HtmlGenerator(private val markdownText: String,
                         private val root: ASTNode,
                         private val providers: Map<NodeType, GeneratingProvider<HtmlGeneratingVisitor>>,
                         private val includeSrcPositions: Boolean = false) {

    private val htmlString: StringBuilder = StringBuilder()

    fun generateHtml(tagRenderer: TagRenderer = DefaultTagRenderer(DUMMY_ATTRIBUTES_CUSTOMIZER, includeSrcPositions)): String {
        HtmlGeneratingVisitor(tagRenderer).visitNode(root)
        return htmlString.toString()
    }

    inner class HtmlGeneratingVisitor(private val tagRenderer: TagRenderer) : TagVisitor {

        override fun visitNode(node: ASTNode) {
            providers[node.type]?.processNode(this, markdownText, node) ?: node.acceptChildren(this)
        }

        override fun visitLeaf(node: LeafNode) {
            providers[node.type]?.processNode(this, markdownText, node) ?: consumeHtml(leafText(markdownText, node))
        }

        override fun consumeTagOpen(node: ASTNode,
                                    tagName: CharSequence,
                                    vararg attributes: CharSequence?,
                                    autoClose: Boolean) {
            htmlString.append(tagRenderer.openTag(node, tagName, *attributes, autoClose = autoClose))
        }

        override fun consumeTagClose(tagName: CharSequence) {
            htmlString.append(tagRenderer.closeTag(tagName))
        }

        override fun consumeHtml(html: CharSequence) {
            htmlString.append(tagRenderer.printHtml(html))
        }
    }

    companion object{
        fun leafText(text: String, node: ASTNode, replaceEscapesAndEntities: Boolean = true): CharSequence {
            if (node.type == NodeType.BLOCK_QUOTE) {
                return ""
            }
            return node.getTextInNode(text)
        }
    }

}

open class DefaultTagRenderer(protected val customizer: AttributesCustomizer,
                              protected val includeSrcPositions: Boolean) : TagRenderer {

    override fun openTag(node: ASTNode, tagName: CharSequence, vararg attributes: CharSequence?, autoClose: Boolean): CharSequence {
        return buildString {
            append("<$tagName")
            for (attribute in customizer.invoke(node, tagName, attributes.asIterable())) {
                if (attribute != null) {
                    append(" $attribute")
                }
            }
            if (includeSrcPositions) {
                append(" ${getSrcPosAttribute(node)}")
            }

            if (autoClose) {
                append(" />")
            } else {
                append(">")
            }
        }
    }

    override fun closeTag(tagName: CharSequence): CharSequence = "</$tagName>"

    override fun printHtml(html: CharSequence): CharSequence = html

    companion object {
        val SRC_ATTRIBUTE_NAME = "md-src-pos"

        fun getSrcPosAttribute(node: ASTNode): CharSequence {
            return "$SRC_ATTRIBUTE_NAME=\"${node.startOffset}..${node.endOffset}\""
        }

        fun trimIndents(text: CharSequence, indent: Int): CharSequence {
            if (indent == 0) {
                return text
            }

            val buffer = StringBuilder()

            var lastFlushed = 0
            var offset = 0
            while (offset < text.length) {
                if (offset == 0 || text[offset - 1] == '\n') {
                    buffer.append(text.subSequence(lastFlushed, offset))
                    var indentEaten = 0

                    eatIndentLoop@
                    while (indentEaten < indent && offset < text.length) {
                        when (text[offset]) {
                            ' ' -> indentEaten++
                            '\t' -> indentEaten += 4 - indentEaten % 4
                            else -> break@eatIndentLoop
                        }
                        offset++
                    }

                    if (indentEaten > indent) {
                        buffer.append(" ".repeat(indentEaten - indent))
                    }
                    lastFlushed = offset
                }

                offset++
            }

            buffer.append(text.subSequence(lastFlushed, text.length))
            return buffer
        }

    }
}